S12-02 数据库-MySQL-JSON
[TOC]
概述
MySQL JSON 语法全解析:
MySQL 5.7 开始原生支持 JSON 数据类型,8.0 做了大量增强,提供了结构化存储+原生查询/修改能力,兼顾非结构化数据的灵活度和关系型数据库的查询性能。以下从数据定义、值构造、查询提取、修改更新、聚合/条件判断、索引优化六个维度,详解 JSON 核心语法,覆盖所有高频操作并附示例。
JSON 数据类型定义
建表时指定 JSON 列
建表时指定 JSON 列:
JSON 是 MySQL 原生数据类型,建表时直接声明,无需指定长度,MySQL 会自动验证 JSON 格式的合法性(非法格式插入会报错)。
-- 基础示例:用户表,扩展字段用 JSON 存储
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`ext_info` JSON COMMENT '用户扩展信息:年龄、爱好、地址',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;允许的 JSON 列属性
允许的 JSON 列属性:
- 支持
DEFAULT(MySQL 8.0.13+),默认值必须是合法的 JSON 常量(如'{}'、'[]'); - 不支持
AUTO_INCREMENT、CHARSET/COLLATE(JSON 内部编码为 utf8mb4); - 支持
NOT NULL,空 JSON 需用'{}'/'[]',而非NULL。
-- MySQL 8.0.13+ 支持 JSON 默认值
CREATE TABLE `goods` (
`id` INT PRIMARY KEY,
`attrs` JSON NOT NULL DEFAULT '{}' COMMENT '商品属性,默认空对象'
);JSON 值构造语法
JSON 值构造语法:
插入/更新 JSON 列时,需构造合法的 JSON 数据,MySQL 提供直接写 JSON 字符串和内置函数构造两种方式,推荐函数构造(自动处理引号转义,避免语法错误)。
直接写入 JSON 字符串
直接写入 JSON 字符串:
要求严格的 JSON 格式:键必须用双引号,值支持字符串(双引号)、数字、布尔、null、数组、对象,单引号会报错。
-- 插入 JSON 对象/数组
INSERT INTO `user` (name, ext_info) VALUES
('张三', '{"age": 20, "hobby": ["篮球", "游戏"], "address": {"province": "广东", "city": "深圳"}}'),
('李四', '{"age": 25, "hobby": ["读书"], "address": null}');内置构造函数
内置构造函数(推荐):
JSON_OBJECT()
:JSON_OBJECT([key, val], ...):构造 JSON 对象
语法:键为字符串(可省略引号),值为任意MySQL类型,自动转换为JSON对应类型,键值对必须成对出现。 示例:
INSERT INTO `user` (name, ext_info)
VALUES ('王五', JSON_OBJECT(
'age', 22,
'hobby', JSON_ARRAY('跑步', '游泳'), -- 嵌套构造数组
'address', JSON_OBJECT('province', '浙江', 'city', '杭州')
));JSON_ARRAY()
:JSON_ARRAY([val], ...):构造 JSON 数组
语法:参数为任意MySQL类型,自动转换为JSON数组元素,无参数则生成空数组。 示例:
SELECT JSON_ARRAY(1, '2', true, null); -- 结果:[1, "2", true, null]JSON_MERGE_PRESERVE()
:JSON_MERGE_PRESERVE():合并多个 JSON 数据
MySQL 8.0 废弃 JSON_MERGE,推荐 JSON_MERGE_PRESERVE(保留所有键值,数组拼接,对象键冲突则保留多个值),还有 JSON_MERGE_PATCH(对象键冲突则覆盖,类似 JSON 补丁)。 语法:JSON_MERGE_PRESERVE(json1, json2, ...)示例:
SELECT JSON_MERGE_PRESERVE('{"a":1}', '{"b":2, "a":3}', '[4,5]');
-- 结果:{"a": [1, 3], "b": 2, "0": 4, "1": 5}
SELECT JSON_MERGE_PATCH('{"a":1, "b":2}', '{"b":3, "c":4}');
-- 结果:{"a": 1, "b": 3, "c": 4}(覆盖b,新增c)JSON_QUOTE()
:JSON_QUOTE():将普通字符串转义为 JSON 字符串
用途:解决普通字符串含双引号/反斜杠的转义问题,生成合法的 JSON 字符串值。 示例:
SELECT JSON_QUOTE('He said "Hello"'); -- 结果:"He said \"Hello\""
INSERT INTO `user` (name, ext_info)
VALUES ('赵六', JSON_OBJECT('desc', JSON_QUOTE('昵称:"小六"')));MySQL 类型与 JSON 类型的自动转换
MySQL 类型与 JSON 类型的自动转换:
| MySQL 类型 | JSON 类型 | 示例 |
|---|---|---|
| INT/BIGINT/DECIMAL | JSON 数字 | 123 → 123 |
| VARCHAR/TEXT | JSON 字符串 | 'test' → "test" |
| BOOL/TINYINT(1) | JSON 布尔 | true → true,0 → false |
| NULL | JSON null | NULL → null |
| DATE/DATETIME | JSON 字符串 | NOW() → "2026-01-22..." |
JSON 数据查询与提取语法
JSON 数据查询与提取语法:
核心是从 JSON 对象/数组中提取指定值,MySQL 提供两种语法风格:箭头运算符(->/->>,简洁)和内置提取函数(JSON_EXTRACT,功能全面),箭头运算符是提取函数的语法糖。
基础提取:JSON 对象
基础提取:JSON 对象(按键取值):
->
:->:提取值,保留 JSON 原生格式(如字符串带双引号)
->>
:->>:提取值,自动转义为 MySQL 普通字符串(无引号,最常用)
JSON_EXTRACT()
:JSON_EXTRACT(json_col, path, ...):原生函数,path 为 JSON 路径
JSON 路径规则:
- 用
$.键表示对象的顶级键; - 用
$.键.子键表示嵌套对象的子键; - 路径中的特殊键(含空格/特殊字符)用
"$.键名"包裹,如$."user name"。
示例:查询用户的年龄、城市、爱好(基于前文 user 表)
-- 箭头运算符(推荐)
SELECT
name,
ext_info->'$.age' AS age, -- 保留JSON格式,数字无引号,字符串有引号
ext_info->>'$.address.city' AS city, -- 转普通字符串,无引号
ext_info->'$.hobby' AS hobby -- 提取整个数组
FROM `user`;
-- 等价的 JSON_EXTRACT 函数
SELECT
name,
JSON_EXTRACT(ext_info, '$.age') AS age,
JSON_UNQUOTE(JSON_EXTRACT(ext_info, '$.address.city')) AS city -- JSON_UNQUOTE 去除引号,等价 ->>
FROM `user`;
-- 提取含特殊字符的键(示例:ext_info 含 "user name" 键)
SELECT ext_info->>'$."user name"' FROM `user`;基础提取:JSON 数组
基础提取:JSON 数组(按下标取值):
JSON 路径规则:数组下标从 0 开始,用 $.数组键[下标] 表示,支持负下标(-1 表示最后一个元素,-2 倒数第二个,MySQL 8.0+)。 示例:提取用户的第一个爱好、最后一个爱好
SELECT
name,
ext_info->>'$.hobby[0]' AS first_hobby, -- 第一个爱好
ext_info->>'$.hobby[-1]' AS last_hobby -- 最后一个爱好(MySQL 8.0+)
FROM `user`;批量提取:JSON_TABLE
批量提取:JSON_TABLE(MySQL 8.0+ 核心函数):
将 JSON 数组转换为关系型表,实现「JSON 数据联表查询」「批量统计」,是处理 JSON 数组的最核心函数,必须掌握。
语法::
JSON_TABLE(
json_expr, -- JSON 数组/表达式
path COLUMNS ( -- 数组元素的路径,* 表示每个元素
列名1 类型 PATH '元素路径1' [AS JSON], -- [AS JSON] 保留JSON格式
列名2 类型 PATH '元素路径2' [DEFAULT '默认值'] ON EMPTY, -- 空值默认
列名3 类型 FOR ORDINALITY -- 生成行号,从1开始
)
) AS 表别名示例:将用户爱好数组转为表,统计每个爱好的出现次数:
-- 步骤1:用 JSON_TABLE 拆分爱好数组;步骤2:分组统计
SELECT
hobby,
COUNT(*) AS count
FROM `user`,
JSON_TABLE(
ext_info->'$.hobby',
'$[*]' COLUMNS (hobby VARCHAR(50) PATH '$') -- $ 表示数组的每个元素
) AS t
GROUP BY hobby
ORDER BY count DESC;高级示例:嵌套 JSON 数组拆分(如 ext_info 含 :[{"course":"数学","score":90},{"course":"语文","score":85}])
-- 拆分学生成绩数组,查询张三的所有科目和分数
SELECT
u.name,
t.course,
t.score
FROM `user` u,
JSON_TABLE(
u.ext_info->'$.score',
'$[*]' COLUMNS (
course VARCHAR(50) PATH '$.course',
score INT PATH '$.score'
) AS t
WHERE u.name = '张三';其他提取函数
其他提取函数:
| 函数 | 用途 | 示例 |
|---|---|---|
JSON_KEYS(json) | 提取 JSON 对象的所有顶级键 | JSON_KEYS(ext_info) → ["age","hobby"] |
JSON_LENGTH(json) | 统计 JSON 长度(对象:键数;数组:元素数) | JSON_LENGTH(ext_info->'$.hobby') → 2 |
JSON_CONTAINS_PATH(json, one/all, path...) | 判断是否存在指定路径(one:任意一个存在;all:所有都存在) | JSON_CONTAINS_PATH(ext_info, 'one', '$.address', '$.phone') → 1(存在address) |
JSON 数据修改与更新语法
JSON 数据修改与更新语法:
MySQL 支持原地修改 JSON 列的指定部分,无需整列更新,提供一系列专门的修改函数,覆盖「新增/修改键值」「数组增删元素」「删除键/元素」等所有场景,均为 MySQL 8.0+ 增强,5.7 仅支持部分。
JSON 对象修改
JSON 对象修改(新增/更新/删除键):
JSON_SET()
:JSON_SET(json, path, val, ...):新增/更新键值(存在则更新,不存在则新增)
最常用,支持嵌套键,值为任意MySQL类型。
-- 给张三新增phone键,更新age为21,修改地址的city为广州
UPDATE `user`
SET ext_info = JSON_SET(ext_info, '$.phone', '13800138000', '$.age', 21, '$.address.city', '广州')
WHERE name = '张三';JSON_INSERT()
:JSON_INSERT(json, path, val, ...):仅新增键值(存在则忽略,不更新)
-- 给李四新增phone键(若已存在则不修改)
UPDATE `user`
SET ext_info = JSON_INSERT(ext_info, '$.phone', '13900139000')
WHERE name = '李四';JSON_REPLACE()
:JSON_REPLACE(json, path, val, ...):仅更新键值(不存在则忽略,不新增)
-- 仅更新张三的age为22(若age不存在则不操作)
UPDATE `user`
SET ext_info = JSON_REPLACE(ext_info, '$.age', 22)
WHERE name = '张三';JSON_REMOVE()
:JSON_REMOVE(json, path, ...):删除指定路径的键/元素
-- 删除张三的phone键,删除李四的address键
UPDATE `user` SET ext_info = JSON_REMOVE(ext_info, '$.phone') WHERE name = '张三';
UPDATE `user` SET ext_info = JSON_REMOVE(ext_info, '$.address') WHERE name = '李四';JSON 数组修改
JSON 数组修改(增删/插入/替换元素):
JSON_ARRAY_APPEND()
:JSON_ARRAY_APPEND(json, path, val, ...):向数组末尾追加元素
-- 给张三的hobby数组追加"健身"
UPDATE `user`
SET ext_info = JSON_ARRAY_APPEND(ext_info, '$.hobby', '健身')
WHERE name = '张三';JSON_ARRAY_INSERT()
:JSON_ARRAY_INSERT(json, path, val, ...):向数组指定下标插入元素
-- 给张三的hobby数组第1个位置(下标0)插入"书法"
UPDATE `user`
SET ext_info = JSON_ARRAY_INSERT(ext_info, '$.hobby[0]', '书法')
WHERE name = '张三';JSON_REMOVE()
:JSON_REMOVE(json, path):删除数组指定下标元素(复用对象删除函数)
-- 删除张三的hobby数组最后一个元素(MySQL 8.0+ 负下标)
UPDATE `user`
SET ext_info = JSON_REMOVE(ext_info, '$.hobby[-1]')
WHERE name = '张三';JSON_ARRAY_REMOVE()
:JSON_ARRAY_REMOVE():MySQL 8.0.31+ 新增,批量删除数组元素
-- 删除hobby数组中的"游戏"和"读书"(按值删除)
UPDATE `user`
SET ext_info = JSON_ARRAY_REMOVE(ext_info, '$.hobby', '游戏', '读书');批量修改:JSON_MERGE_PATCH
批量修改:JSON_MERGE_PATCH(覆盖式合并):
将新的 JSON 补丁覆盖到原 JSON 对象,实现批量更新,键冲突则覆盖,无则新增,适合多键批量修改。
-- 批量更新张三的ext_info:覆盖age,新增email,保留其他键
UPDATE `user`
SET ext_info = JSON_MERGE_PATCH(ext_info, '{"age":23, "email":"zhangsan@test.com"}')
WHERE name = '张三';JSON 条件判断与聚合语法
条件判断:JSON_CONTAINS
条件判断:JSON_CONTAINS(判断是否包含指定值):
语法:JSON_CONTAINS(json, val [, path]),判断 JSON 数据中是否包含指定值,val 必须是合法的 JSON 格式(如字符串需双引号,用 JSON_QUOTE 构造)。 示例:查询爱好包含「篮球」的用户,查询地址省份为「广东」的用户
-- 爱好包含篮球(val为JSON数组,判断数组是否包含该元素)
SELECT * FROM `user` WHERE JSON_CONTAINS(ext_info->'$.hobby', JSON_QUOTE('篮球'));
-- 地址省份为广东(val为JSON字符串,判断对象键值是否匹配)
SELECT * FROM `user` WHERE JSON_CONTAINS(ext_info->'$.address', JSON_QUOTE('广东'), '$.province');模糊查询:JSON_SEARCH
模糊查询:JSON_SEARCH(按值查路径,支持模糊匹配):
语法:JSON_SEARCH(json, one/all, search_str [, escape_char [, path...]])
one:找到第一个匹配值就返回路径;all:返回所有匹配值的路径(数组);search_str:支持通配符%(任意字符)、_(单个字符),类似 MySQLLIKE;- 返回值:JSON 路径字符串,无匹配则返回
NULL。
示例:查询爱好包含「球」的用户,查询地址含「深」的用户
-- 爱好包含"球"(通配符%),one表示找到第一个即可
SELECT * FROM `user` WHERE JSON_SEARCH(ext_info->'$.hobby', 'one', '%球%') IS NOT NULL;
-- 地址的任意子键含"深"(path为$.address,表示遍历address的所有子键)
SELECT * FROM `user` WHERE JSON_SEARCH(ext_info, 'one', '%深%', null, '$.address') IS NOT NULL;JSON 数据聚合计算
JSON 数据聚合计算:
直接对提取的 JSON 数值进行聚合(SUM/AVG/MAX/MIN/COUNT),提取的数值用 -> 保留数字格式,或 ->> 转成 MySQL 数值类型均可,MySQL 会自动隐式转换。 示例:统计用户的平均年龄、最大年龄,统计爱好数量大于1的用户数
-- 平均年龄、最大年龄
SELECT AVG(ext_info->'$.age') AS avg_age, MAX(ext_info->'$.age') AS max_age FROM `user`;
-- 爱好数量大于1的用户数
SELECT COUNT(*) FROM `user` WHERE JSON_LENGTH(ext_info->'$.hobby') > 1;JSON 索引优化
JSON 索引优化(核心性能点):
原生 JSON 列本身不支持直接建索引,但 MySQL 提供两种索引方案,解决 JSON 数据的查询性能问题,生产环境必须做索引优化,否则大表查询会全表扫描。
虚拟列 + 普通索引
虚拟列 + 普通索引(MySQL 5.7+,最常用):
原理:
原理::
基于 JSON 列的指定路径创建虚拟列(生成列),再对虚拟列建普通索引,查询时通过虚拟列过滤,MySQL 会自动优化执行计划。
步骤:
步骤::
- 建表时/建表后创建持久化虚拟列(
STORED,存储到磁盘,查询更快); - 对虚拟列创建普通 B 树索引;
- 查询时直接使用虚拟列(或用 JSON 路径,MySQL 会自动关联虚拟列)。
示例:给用户的 age 键、address.province 键建索引
示例:给用户的 age 键、address.province 键建索引:
-- 步骤1:给已存在的user表添加虚拟列
ALTER TABLE `user`
ADD COLUMN `age` INT STORED AS (ext_info->>'$.age'), -- 基于$.age的虚拟列
ADD COLUMN `province` VARCHAR(50) STORED AS (ext_info->>'$.address.province');
-- 步骤2:对虚拟列建索引
CREATE INDEX idx_user_age ON `user`(age);
CREATE INDEX idx_user_province ON `user`(province);
-- 步骤3:查询(两种方式均可,MySQL 会自动走索引)
SELECT * FROM `user` WHERE age = 20; -- 直接用虚拟列(推荐)
SELECT * FROM `user` WHERE ext_info->>'$.age' = 20; -- 用JSON路径,MySQL自动优化JSON 多值索引
JSON 多值索引(MySQL 8.0.17+,针对数组):
原理:
原理::
对 JSON 数组创建多值索引,支持数组的 JSON_CONTAINS/JSON_SEARCH 查询走索引,解决数组查询的性能问题,无需创建虚拟列。
语法:CREATE INDEX 索引名 ON 表名((CAST(json_col->'$.数组键' AS JSON ARRAY)));
示例:给用户的 hobby 数组建多值索引
示例:给用户的 hobby 数组建多值索引:
-- 给hobby数组建多值索引
CREATE INDEX idx_user_hobby ON `user`((CAST(ext_info->'$.hobby' AS JSON ARRAY)));
-- 查询爱好包含篮球的用户,自动走索引
SELECT * FROM `user` WHERE JSON_CONTAINS(ext_info->'$.hobby', JSON_QUOTE('篮球'));索引使用注意事项
索引使用注意事项:
- 虚拟列建议用
STORED(持久化),而非VIRTUAL(计算型,查询时计算,性能差); - 多值索引仅支持 JSON 数组,且仅对
JSON_CONTAINS/JSON_SEARCH优化; - 避免对高频修改的 JSON 路径建索引,索引会增加更新开销;
- 用
EXPLAIN验证索引是否生效,生效则type为ref/range,key显示索引名。
MySQL JSON 版本兼容性说明
MySQL JSON 版本兼容性说明:
| 功能/函数 | MySQL 5.7 | MySQL 8.0+ |
|---|---|---|
| JSON 数据类型 | ✔️ | ✔️(增强验证) |
| ->/->> 箭头运算符 | ✔️ | ✔️(支持负下标) |
| JSON_OBJECT/JSON_ARRAY | ✔️ | ✔️ |
| JSON_TABLE | ❌ | ✔️(核心增强) |
| 负下标(hobby[-1]) | ❌ | ✔️ |
| JSON 列 DEFAULT 值 | ❌ | 8.0.13+ ✔️ |
| 多值索引 | ❌ | 8.0.17+ ✔️ |
| JSON_MERGE_PATCH/PRESERVE | ❌ | ✔️(废弃 JSON_MERGE) |
| JSON_ARRAY_REMOVE | ❌ | 8.0.31+ ✔️ |
核心最佳实践
核心最佳实践:
- 适用场景:存储非结构化/半结构化数据(如用户扩展信息、商品属性)、高频新增字段但不想改表结构的场景;不适用:需要频繁联表/聚合、数据结构固定的场景(优先用普通列)。
- 格式规范:插入时优先用
JSON_OBJECT/JSON_ARRAY构造,避免手动写 JSON 字符串导致的转义错误。 - 查询规范:处理 JSON 数组优先用
JSON_TABLE,实现关系型查询;单值提取用->>箭头运算符,简洁高效。 - 性能规范:大表必须建虚拟列索引/多值索引,避免全表扫描;控制 JSON 列的大小,不要存储超大 JSON(如超过100KB,建议用 TEXT 并手动解析)。
- 版本选择:生产环境优先用 MySQL 8.0+,5.7 缺失大量核心功能(如 JSON_TABLE),性能和灵活性较差。
- 避免滥用:不要将所有字段都塞到 JSON 列,核心查询字段(如用户ID、商品价格)仍用普通列,JSON 仅存扩展字段。
以上为 MySQL JSON 所有核心语法,覆盖开发、查询、更新、优化全流程,结合示例可直接在生产环境使用,重点掌握箭头运算符、JSON_TABLE、虚拟列索引三个核心点,即可解决 99% 的 JSON 开发场景。